原文来自Error handling and Go

背景介绍

如果你有写过Go代码,那么你可以会遇到Go中内建类型error。Go语言使用error*值来显示异常状态。例如,os.Open在打开文件错误时,会返回一个非nil error值。

func Open(name string) (file *File, err error)

下面的代码使用os.Open来打开一个文件。如果出现错误,会调用log.Fatal打印出错误的信息并且终止代码。

f, err := os.Open("filename.etx")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

在使用Go的工作中,上面的例子已经能满足大多数情况,但是这篇文章会更进一步的探讨关于捕获异常的实践。

error类型

error类型是一个interface类型。一个error变量可以通过任何可以描述自己的string类型的值来展示自己。下面是它的接口描述:

type error interface {
    Error() String
}

error类型,就像其他内建类型一样,==是在全局中预先声明的==。这意味着我们不用导入就可以在任何地方使用它。

最常用的error实现是在 errors 包中定义的一个不可导出的类型:errorString

// errorString is a trivial implementation os error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

通过errors.New函数可以创建一个errorString实例.该函数接收一个string参数,并将string参数转换为一个erros.errorString,然后返回一个error值.

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

下面是如何使用errors.New的例子

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, error.New("math: squara root of negative number")
    }
    // implementation
}

在调用Sqrt时,如果传入的参数是负数,调用者会接收到Sqrt返回的一个非空error值(正确来说应该是一个errors.errorString值)。调用者可以通过调用errorError方法或者通过打印来得到错误信息字段("math: squara root of nagative number")。

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt包通过调用Error()方法来格式化error

一个error接口的责任是总结错误的内容。os.Open的错误返回的格式是像"open /etc/passwd: permission denied"这样的格式, 而不仅仅只是"permission denied"。Sqrt返回的错误缺少了关于非法参数的信息。

为了让信息更加明确,比较好用的一个函数是fmt包里面的Errorf。它根据Printf的规则来函格式化一个字符串并且返回,就像使用errors.New创建的error值。

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

很多情况下,fmt.Errorf已经能够满足我们了,但是有时候我们还需要更多的细节。我们知道error是一个接口,因此你可以定义任意的数据类型来作为error值,以供调用者获取更多的错误细节。

例如,如果有一个比较复杂的调用者想要恢复传给Sqrt的非法参数。我们通过定义一个新的错误实现而不是使用errors.errorString来实现这个需求:

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %s", float64(f))
}

一个复杂的调用者就可以使用类型断言(type assertion)来检测NegativeSqrtError并且捕获它,与此同时,对于使用fmt.Println或者log.Fatal来输出错误的方式来说却没有改变他们的行为。

另一个例子来自json包,当我们在使用json.Decode函数时,如果我们传入了一个不合格的JSON字段,函数返回SyntaxError类型错误。

type SyntaxError struct {
    msg     string // description of error
    Offset  int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

我们可以看到, Offset甚至还没有在默认的errorError函数中出现,但是调用者可以用它来生成带有文件名和行号的错误信息。

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

(这是项目Camlistore中的代码的一个简化版实现)

内置的error接口只需要实现Error方法;特定的error实现可能会添加其他的一些附加方法。例如net包, net包内有很多种error类型,通常跟常用的error一样,但是有些error实现添加一些附加方法,这些附加方法通过net.Error接口定义:

package net

type Error interface {
    error
    Timeout() bool  // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

客户端代码可以通过类型断言来检测一个net.Error错误以区分这是一个暂时性错网络误还是一个永久性错误。例如当一个网络爬虫遇到一个错误时,如果是暂时性错误,它会睡眠一下然后在重试,否则停止尝试。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

简化捕获重复的错误

Go中,错误捕获是很重要的。Go的语言特性和使用习惯鼓励你在错误发生时做出明确的检测(这和那些抛出异常的然后有时捕获他们的语言有些区别)。在某些情况,这种方式会造成Go代码的冗余,不过幸运的是我们能使用一些技术来减少这种重复的捕获操作。

考虑这样一个App应用,这个应用有一个HTTP的处理函数,用来从数据库接收数据并且将数据用模板格式化。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengin.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormatValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return 
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
    
}

这个函数捕获从datastore.Get函数和viewTemplate.Excute方法返回的错误。这两种情况都返回带Http状态码为500的简单的错误信息。上面的代码看起来也不多,可以接受,但是如果添加更多的 HTTP handlers情况就不一样了,你马上会发现很多这样的重复代码来处理这些错误。

为了减少这些重复的错误处理代码,我们可以定义我们自己的 HTTP AppHandler,让它成一个带着error返回值的类型:

type appHandler func(http.ResponseWriter, *http.Request) error

然后我们可以更改viewRecord函数,让它将错误返回:

fun viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appending.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValie("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

这看起来比原始版本代码的简单了些, 但是 http 包并不能理解viewRecord函数返回的错误。这时我们可以通过实现在appHandler上的 http.Handler接口的方法 ServerHTTP来解决这个问题:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

ServeHTTP方法调用appHandler方法并且将返回的错误展示给用户。注意,ServeHTTP方法的接受者是一个函数。(go语言允许这样做)这个方法通过表达式fn(w, r)来调用他的接受者,使ServeHTTP和appHandler关联在一起
现在,我们在http包中注册viewRecord时,使用了Hanlder函数(而不是HandlerFunc)。因为现在appHandler是一个http.Handler(而不是 http.HandlerFunc)。

func init() {
    http.Handle("/view", appHander(viewRecord))
}

通过构建一个特定的error作为基础构建,我们可以让我们的错误对用户更友好。相对于仅仅将错误字符串展示给出来,返回带有HTTP状态码的错误字符串是一个更好的展示方式,并且还能记录下所有的错误信息以供App开发者调试用。

下面的代码展示如何实现这种需求。我们创建了一个包含error类型的和其他类型的字段的appError结构体

type appError struct {
    Error   error
    Message string
    Code    int
}

下一步我们修改appHandler类型,让它返回 *appError值:

type appHandler func(http.ResponseWriter, *http.Request) * appError

(通常,相对于返回一个error返回一个特定类型的错误是不对的,具体原因可以参考Go FQA , 但是在这里是正确的,因为这个错误值只有ServeHTTP会用到它)

然后我们让appHandler的ServeHTTP方法将带着HTTP状态码的appError错误信息展示给用户,并且将所有错误信息展示给开发者终端。

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最后,我们更新viewRecord的代码,让它遇到错误时返回更多的内容:

func viewRecord(w http.ResponseWrite, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError(err, "Can't display record", 500)
    }
    return nil
}

这个版本的viewRecord跟原始版本有着相同的长度,但是现在这些放回信息都有特殊的信息,我们提供了更为友好的用户体验。

当然,这还不是最终的方案,我们还可以进一步提升我们的application中的error处理方式。下面是改进的一些点:

  • 给错误handler提供一个漂亮的HTML模板
  • 如果用户是超级用户的话,添加堆叠追踪到HTTP响应中,更方便调试
  • appError写一个构造函数来存储stack trace来让开发者调试更方便
  • 恢复appHandler中的panic,用Critical级别的log将错误记录到终端,同时告诉用户"a serious error has occurred." 这是一个优雅的方式来避免将程序返回的难以理解的错误暴露给用户。关于panic恢复,读者可以参考Defer, Panic, and Recover这篇文章来获取更多的信息。

结论

适合的错误处理是一个好软件最基本的要求。通过这篇文章中讨论的技术,你应该能写出更加可靠简介的Go代码。

参考资料:

Go by Example: Errors


qitianchan
0 声望0 粉丝